iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
Software Development

從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰系列 第 24

[Day 24] 硬體通訊的眉角:time.sleep() 帶來的災難與 Debounce (防彈跳) 實戰

  • 分享至 

  • xImage
  •  

大家好,我是 [您的名字/暱稱]。在前面的天數裡,我們探討了多執行緒 (Multi-threading) 的架構、優先級設計,以及如何優雅地清理那些失控的 Thread。今天,我們要將鏡頭拉近,來看一個在軟硬體整合時,幾乎所有工程師都曾踩過的地雷:time.sleep()

在寫純軟體時,time.sleep() 是一個方便的工具;但在與硬體通訊(尤其是 Serial/UART、按鈕狀態、LED 控制)的世界裡,它卻可能是摧毀整個系統即時性的元兇。今天我們將以 DataDock 為例,探討為什麼我們必須戒掉 time.sleep(),以及如何利用非阻塞 (Non-blocking) 計時器與軟體防彈跳 (Debounce) 來打造一個反應靈敏的系統。

情境還原:time.sleep() 造成的「假死」現象

在硬體通訊中,我們經常遇到「需要等待硬體準備好」的情境。例如:

  1. 送出一個指令給 ST-50 設備後,硬體需要 200 毫秒才會有回應。
  2. LED 燈需要每 500 毫秒閃爍一次。

最直覺(也是最危險)的寫法就是:

# 絕對不要在主迴圈或通訊執行緒這樣寫
def wait_for_hardware():
    serial.write(b'GET_DATA')
    time.sleep(0.2)  # 等待硬體回應
    return serial.read_all()

為什麼這是個災難?

  1. 執行緒被徹底凍結 (Blocking):在 time.sleep(0.2) 的期間,這條 Thread 什麼事都不能做。它不能接收中斷訊號,不能更新系統狀態,甚至無法提早結束程式(這也是為什麼我們在 Day 23 強調 Thread 難以優雅關閉的原因之一)。
  2. 錯失黃金時機:如果硬體在 10 毫秒後就回應了,你的程式卻還在傻傻睡覺,平白浪費了 190 毫秒。這在要求 Soft Real-Time (軟即時) 的資料擷取系統中,會造成緩衝區溢位或封包遺失。
  3. UI/LED 凍結:如果這個 Sleep 發生在處理 LED 燈號的 Thread,使用者就會看到燈號「卡住」或「反應遲鈍」,這在醫療或工業設備上會引發極大的不安全感。

解決方案:非阻塞 (Non-blocking) 狀態機

要取代 time.sleep(),我們必須擁抱狀態機 (State Machine)時間差計算。其核心思想是:「不要睡覺,而是不斷去問手錶現在幾點了」。

實作:利用 time.time() 進行非阻塞等待

我們可以將原本死等的邏輯,改寫成這樣的非阻塞輪詢 (Polling):

import time

class HardwareCommunicator:
    def __init__(self):
        self.last_cmd_time = 0
        self.timeout = 0.2
        self.waiting_for_reply = False

    def loop(self):
        current_time = time.time()
        
        # 如果正在等待硬體回應,檢查是否超時
        if self.waiting_for_reply:
            if current_time - self.last_cmd_time >= self.timeout:
                print("硬體回應超時!")
                self.waiting_for_reply = False
            else:
                # 檢查是否有資料進來,如果有就提早處理,不用傻等!
                if serial.in_waiting > 0:
                    data = serial.read_all()
                    self.process_data(data)
                    self.waiting_for_reply = False
            return # 本次迴圈結束,立刻把 CPU 讓給別人

        # 若沒有在等待,則執行其他工作或發送下一個指令...

在 DataDock 的 sync_engine.pyled.py 中,我們大量使用了這種技巧。這確保了系統能夠在每一毫秒內迅速反應使用者的拔插動作 (Plug/Unplug),而不會因為某個 sleep 而卡在當下的狀態。

進階應用:軟體防彈跳 (Debounce)

在拔插 USB (或按下硬體按鈕) 的瞬間,電氣訊號往往是不穩定的。在示波器下,訊號會在 High 和 Low 之間快速震盪(即 Bounce),這會導致作業系統或軟體誤以為設備被「快速插拔了無數次」。

這就是為什麼在前面的天數中,我們遇到了 "Software Re-plug" 的亂象,導致 Thread 瘋狂重生。為了解決這個問題,我們必須在軟體層導入 Debounce(防彈跳)機制。

Debounce 的核心邏輯

Debounce 的目的是:只有當狀態維持穩定超過一段特定時間(例如 500 毫秒)後,我們才承認這個狀態的改變。

以下是我們在 DataDock 中實作 USB 連線狀態防彈跳的概念程式碼:

import time

class ConnectionMonitor:
    def __init__(self):
        self.current_state = False
        self.last_stable_state = False
        self.last_debounce_time = time.time()
        self.debounce_delay = 0.5  # 500 毫秒防彈跳時間

    def update_hardware_state(self, raw_state):
        current_time = time.time()
        
        # 如果硬體偵測到的原始狀態,跟我們目前紀錄的不一樣,代表訊號正在變動
        if raw_state != self.current_state:
            self.last_debounce_time = current_time
            self.current_state = raw_state

        # 如果狀態已經維持不變超過 debounce_delay,我們才正式採納它
        if (current_time - self.last_debounce_time) > self.debounce_delay:
            if self.current_state != self.last_stable_state:
                self.last_stable_state = self.current_state
                
                # 只有在這裡,才真正觸發「連線」或「斷線」的事件
                if self.last_stable_state:
                    print("🚀 設備已穩固連接,啟動連線程序!")
                else:
                    print("🛑 設備已徹底斷開,啟動清理程序!")

[!TIP]
軟硬體整合的黃金法則
將系統核心設計為事件驅動 (Event-driven) 或定時輪詢 (Polling) 的架構,不僅能有效解決 Bounce 造成的干擾,還能讓硬體故障或卡死時,軟體依然能維持基本運作,大幅提升系統的容錯率。

小結

今天我們分享了在軟硬體通訊時,兩個非常實用的心法:

  1. 用非阻塞的狀態機取代 time.sleep():把時間控制權交還給主迴圈,系統反應速度會大幅提升。
  2. 利用時間差實現軟體防彈跳 (Debounce):過濾掉硬體電氣訊號的雜訊,讓軟體行為更加穩定可靠。

掌握了這兩個技巧,你的 Python 程式在面對實體世界的各種不穩定因素時,就能顯得游刃寫意。

明天(Day 25),我們將把這幾天學到的多執行緒、狀態機、非阻塞輪詢,全部收斂到一個終極設計模式:「有限狀態機 (FSM)」,來看看如何用更清晰的架構,掌控複雜的連線與同步邏輯!我們明天見!


上一篇
Day 23 - 如何優雅地殺死 Python Thread?世代計數器 (Generation Counter) 的妙用
下一篇
Day 25:從隱性狀態到嚴謹架構:淺談有限狀態機 (FSM) 如何徹底解決爭奪鬧劇
系列文
從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言